Average Rank

silver - 3 Image silver - 3 | 75 lp (including bronze 3/unranked)

silver - 1 Image silver - 1 | 52 lp (excluding bronze 3/unranked)

## Rows: 11521 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): match_uid, gamemode
## dbl (1): replay_id
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

match_players <- match_players %>%
  filter(hero_data_playtime >= 30)  # Remove short swaps

match_players <- match_players %>%
  group_by(match_uid) %>%
  mutate(
    is_draw = all(is_win == FALSE)  # If no one won, it's a draw
  ) %>%
  ungroup()


# Step 2: Calculate each hero's percentage of the total match duration per player
match_players <- match_players %>%
  group_by(match_uid, player_uid) %>%
  mutate(
    playtime_percentage = hero_data_playtime / sum(hero_data_playtime)
  ) %>%
  ungroup()

# Step 3: Merge hero names
match_players <- match_players %>%
  mutate(hero_data_hero_id = as.character(hero_data_hero_id)) %>%
  left_join(hero_stats, by = c("hero_data_hero_id" = "hero_data_hero_id"))

# Step 4: Create team compositions
team_comps <- match_players %>%
  group_by(match_uid, is_win, player_uid) %>%
  summarise(heroes = paste(sort(unique(name.y)), collapse = "/"), .groups = "drop") %>%
  group_by(match_uid, is_win) %>%
  summarise(team = paste(sort(unique(heroes)), collapse = ", "), .groups = "drop")

# Step 5: Match each team against its opponent
matchups <- team_comps %>%
  pivot_wider(names_from = is_win, values_from = team, names_prefix = "team_") %>%
  filter(!is.na(team_TRUE) & !is.na(team_FALSE))  # Ensure both teams exist

# Step 6: Compute individual hero matchups (with playtime adjustments)
hero_matchups <- match_players %>%
  select(match_uid, player_uid, name.y, playtime_percentage, is_win, is_draw) %>%
  rename(hero = name.y)

# Step 7: Join the opposing team members per match (ensuring only enemy matchups)
hero_matchups <- hero_matchups %>%
  inner_join(
    hero_matchups %>%
      rename(opponent_hero = hero, opponent_playtime = playtime_percentage, is_win_opponent = is_win, player_uid_opponent = player_uid),
    by = c("match_uid"),
    suffix = c("_self", "_opponent")
  ) %>% 
  select(-is_draw_opponent) %>%
  filter(
    hero != opponent_hero,        # Remove self-matches
    player_uid != player_uid_opponent, # Ensure we're not matching the same player
    is_win != is_win_opponent,    # Ensure we're only comparing opponents
    !is_draw_self                          # Exclude draws
  )
## Warning in inner_join(., hero_matchups %>% rename(opponent_hero = hero, : Detected an unexpected many-to-many relationship between `x` and `y`.
## ℹ Row 1 of `x` matches multiple rows in `y`.
## ℹ Row 1 of `y` matches multiple rows in `x`.
## ℹ If a many-to-many relationship is expected, set `relationship =
##   "many-to-many"` to silence this warning.
hero_matchups <- hero_matchups %>% select(-is_draw_self)


# Step 8: Compute partial wins/losses per hero matchup
hero_matchups <- hero_matchups %>%
  mutate(
    weighted_win = ifelse(is_win, playtime_percentage * opponent_playtime, 0),
    weighted_loss = ifelse(!is_win, playtime_percentage * opponent_playtime, 0)
  )

# Step 9: Aggregate hero counter data
hero_counters <- hero_matchups %>%
  group_by(hero, opponent_hero) %>%
  summarise(
    wins = sum(weighted_win),
    losses = sum(weighted_loss),
    total_matches = wins + losses,
    win_rate = (wins / total_matches) * 100,
    .groups = "drop"
  ) %>%
  arrange(desc(win_rate))

# Step 10: Apply a threshold to filter out rare matchups
total_matches <- nrow(matches)
threshold <- total_matches * 0.01  # 1% of all matches

hero_counters <- hero_counters %>%
  filter(total_matches >= threshold)

# Step 11: Compute overall hero performance (with playtime adjustments)
hero_stats_counters <- hero_matchups %>%
  group_by(hero) %>%
  summarise(
    total_wins = sum(weighted_win),
    total_losses = sum(weighted_loss),
    total_matches = total_wins + total_losses,
    win_rate = (total_wins / total_matches) * 100,
    .groups = "drop"
  )

# Step 12: Merge role and icon data for visualization
hero_counters <- hero_counters %>%
  left_join(select(all_heroes, name, role, icon), by = c("hero" = "name")) %>%
  rename(role_win = role, icon_win = icon) %>%
  left_join(select(all_heroes, name, role, icon), by = c("opponent_hero" = "name")) %>%
  rename(role_lose = role, icon_lose = icon)
library('ggforce')
# Convert data to long format for ggplot

# Table Format
# Table Format with Icons and Role-based Structure
get_top_counters <- function(df, hero_name, role, strongest = TRUE) {
  filtered_df <- df %>%
    filter(hero == hero_name & role_lose == role) %>%
    arrange(if (strongest) desc(win_rate) else win_rate) %>%
    head(3)  # Select top 3

  if (nrow(filtered_df) == 0) return("") # Return empty if no matchups

   return(paste0(
    '<div style="display: inline-block; text-align: center; margin: 5px;">
      <div style="font-size: 12px; font-weight: bold; color: white; background: ',
      ifelse(100-filtered_df$win_rate > 50, 'green', 'red'),
      '; padding: 2px; border-radius: 5px;">',
        round(100-filtered_df$win_rate, 1), '%</div>
      <img src="', filtered_df$icon_lose, '" height="30px" style="display: block; margin-top: 2px;">
    </div>',
    collapse = " "
  ))
}

# Create table with strongest & weakest counters per role for each hero
hero_counters_table <- hero_counters %>%
  distinct(hero, role_win, icon_win) %>%
  rowwise() %>%
  mutate(
    strongest_vanguard_counters = get_top_counters(hero_counters, hero, "VANGUARD", FALSE),
    strongest_duelist_counters = get_top_counters(hero_counters, hero, "DUELIST", FALSE),
    strongest_strategist_counters = get_top_counters(hero_counters, hero, "STRATEGIST", FALSE),
    weakest_vanguard_counters = get_top_counters(hero_counters, hero, "VANGUARD", TRUE),
    weakest_duelist_counters = get_top_counters(hero_counters, hero, "DUELIST", TRUE),
    weakest_strategist_counters = get_top_counters(hero_counters, hero, "STRATEGIST", TRUE)
  ) %>%
  ungroup()

# Merge hero_stats win_rate into hero_counters_table
hero_counters_table <- hero_counters_table %>%
  left_join(hero_stats %>% select(name, win_rate, pick_rate), by = c("hero" = "name")) %>%
  arrange(desc(win_rate)) %>% 
  mutate(
    pick_rate = paste0(round(pick_rate,2),'%'),
    win_rate = paste0(win_rate,'%'),
  )
# Convert heroes_win to image format for display


hero_counters_table <- hero_counters_table %>%
  mutate(icon_win = paste0(
    '<div style="position: relative; display: inline-block;">
       <img src="', icon_win, '" height="50px" style="border-radius: 10px;">
       <div style="position: absolute; bottom: 4px; left: 0px; background: rgba(0, 0, 0, 0.5); border-radius: 3px; padding: 1px 3px; height: 18px; display: flex; align-items: center;">
         <img src="https://rivalskins.com/wp-content/uploads/marvel-assets/ui/roles/', tolower(role_win), '.png" 
              height="14px" style="display: block;">
       </div>
     </div>'
  ))






hero_counters_table <- hero_counters_table %>%
  rename("Hero" = icon_win,
         "Winrate" = win_rate,
         "Pickrate" = pick_rate,
         "Best Tank to Pick" = strongest_vanguard_counters,
         "Best Dps to Pick" = strongest_duelist_counters,
         "Best Support to Pick" = strongest_strategist_counters,
         "Worst Tank to Pick" = weakest_vanguard_counters,
         "Worst Dps to Pick" = weakest_duelist_counters,
         "Worst Support to Pick" = weakest_strategist_counters,
         )


# Create interactive datatable
datatable(hero_counters_table, escape = FALSE, options = list(pageLength = 10, autoWidth = TRUE, columnDefs = list(
    list(targets = c(1,2), visible = FALSE)  # Adjust these numbers based on step 1
  ))) %>%
  formatStyle(
    columns = names(hero_counters_table),  # Apply to all columns
    `text-align` = "center"  # Center-align text
  ) 
library(dplyr)
library(tidyr)
library(cluster)  # For hierarchical clustering
library(dbscan)   # For DBSCAN
library(stringr)

# Ensure team compositions contain exactly 6 heroes (and no duplicates)
team_comps <- match_players %>%
  group_by(match_uid, is_win, player_uid) %>%
  summarise(heroes = sort(unique(name.y)), .groups = "drop") %>%
  filter(length(heroes) == 6) %>%   # Only allow valid 6-hero compositions
  mutate(team = paste(heroes, collapse = "/")) %>%
  group_by(team) %>%
  summarise(
    win_rate = mean(is_win) * 100,
    games_played = n(),
    .groups = "drop"
  ) %>%
  arrange(desc(games_played))  

# Convert teams into binary feature vectors (one-hot encoding)
hero_list <- unique(unlist(strsplit(paste(team_comps$team, collapse = "/"), "/")))
hero_matrix <- do.call(rbind, lapply(team_comps$team, function(t) {
  as.integer(hero_list %in% unlist(strsplit(t, "/")))
}))
colnames(hero_matrix) <- hero_list

# Perform DBSCAN clustering
dbscan_result <- dbscan(hero_matrix, eps = 1.5, minPts = 3)  # Adjust eps for better grouping
team_comps$cluster <- dbscan_result$cluster

# View cluster assignments
team_comps %>% arrange(cluster, desc(games_played))
 

Created by

Jods